iT邦幫忙

2025 iThome 鐵人賽

DAY 3
0
Software Development

Polars熊霸天下系列 第 3

[Day03] - Polars帶來了什麼便利

  • 分享至 

  • xImage
  •  

今天我們透過幾個例子來觀察,當使用純Python及Polars時,其各自是如何解決問題,進而從中了解Polars帶來了什麼便利。

本日大綱如下:

  1. 本日引入模組
  2. 重覆類似操作
  3. 條件選擇
  4. 分組聚合
  5. 容器內元素選取方式
  6. 實際範例
  7. codepanda

0. 本日引入模組

import pandas as pd
import polars as pl
import polars.selectors as cs
from itertools import groupby

1. 重覆類似操作(*1)

假設我們有一組列表lucky_numbers,內含五個幸運數字:

lucky_numbers = [5, 93, 42, 55, 74]

如果想將lucky_numbers內每一個元素加1,可以使用迴圈:

[number+1 for number in lucky_numbers]

或是使用map

list(map(lambda x: x+1, lucky_numbers))

皆可以得到:

[6, 94, 43, 56, 75]

如果是使用Polars來操作的話,可以使用pl.DataFrame.select()來達成:

df_lucky_numbers = pl.DataFrame({"lucky_number": lucky_numbers})
df_lucky_numbers.select(pl.col("lucky_number").add(1))
shape: (5, 1)
┌──────────────┐
│ lucky_number │
│ ---          │
│ i64          │
╞══════════════╡
│ 6            │
│ 94           │
│ 43           │
│ 56           │
│ 75           │
└──────────────┘

其中:

  • pl.DataFrame.select()是一種context,代表我們想要選取括號中expr所產生的結果。
  • pl.col("lucky_number")是一個expr,代表我們選取了「"lucky_number"」這一列。我們於此expr後接上add(1),代表我們想將「"lucky_number"」這一列的每個欄位都加上1。

Polars讓我們可以不用使用迴圈,而以為中心且以expr來建構所有操作,這即是向量化操作(Vectorized operation)。

2. 條件選擇(*2)

如果我們只想選出lucky_numbers中,大於50的元素,可以這麼做:

[number for number in lucky_numbers if number > 50]

或是使用filter

list(filter(lambda x: x > 20, lucky_numbers))

皆可以得到:

[93, 55, 74]

如果是使用Polars來操作的話,可以使用pl.DataFrame.filter()來達成:

df_lucky_numbers.filter(pl.col("lucky_number") > 50)
shape: (3, 1)
┌──────────────┐
│ lucky_number │
│ ---          │
│ i64          │
╞══════════════╡
│ 93           │
│ 55           │
│ 74           │
└──────────────┘

其中:

  • pl.DataFrame.filter()是一種context,其括號內的expr必須回傳布林值。而pl.DataFrame.filter()可以幫助我們選擇結果為True的行。
  • pl.col("lucky_number")是一個expr,代表我們選取了「"lucky_number"」這一列。我們於此expr後接上> 50,代表我們想知道「"lucky_number"」這一列的每個欄位,其值是否大於50。如果大於50,回傳True,反之則回傳False

Polars讓我們可以使用pl.DataFrame.filter()來篩選所需行數。請留意,此時我們仍然是以為中心來思考,只是需要將條件篩選的expr置入pl.DataFrame.filter()中。

3. 分組聚合(*3)

假設我們有一組列表names,內有五個英文名:

names = ["May", "Jeff", "Cathy", "Jack", "David"]

如果我們想針對名字長度來作分組,可以這麼做:

groups, uniquekeys = [], []

for k, g in groupby(sorted(names, key=len), key=len):
    groups.append(list(g))
    uniquekeys.append(k)

print(f"{uniquekeys=}")
print(f"{groups=}")
uniquekeys=[3, 4, 5]
groups=[['May'], ['Jeff', 'Jack'], ['Cathy', 'David']]

此段程式碼節錄自Python官方說明文檔(註1)。

如果是使用Polars來操作的話,可以使用pl.DataFrame.group_by()來達成:

df_names = pl.DataFrame({"name": names})

(
    df_names.group_by(pl.col("name").str.len_bytes().alias("len"))
    .agg(pl.col("name"))
    .sort(pl.col("len"))
)
shape: (3, 2)
┌─────┬────────────────────┐
│ len ┆ name               │
│ --- ┆ ---                │
│ u32 ┆ list[str]          │
╞═════╪════════════════════╡
│ 3   ┆ ["May"]            │
│ 4   ┆ ["Jeff", "Jack"]   │
│ 5   ┆ ["Cathy", "David"] │
└─────┴────────────────────┘

其中:

  • pl.DataFrame.group_by()是一種context,其括號內的expr為分組的目標。pl.col("name").str.len_bytes().alias("len")是一個expr,代表我們選取了「"names"」這一列。接著我們使用了str這個accessor,來呼叫len_bytes(),最後再利用.alias("len")重名命名此列為「"len"」。
  • .agg()可以想成是pl.DataFrame.group_by()的後續動作,其括號內的expr為該分組內元素所需進行的聚合計算。pl.col("name")是一個expr,其置於.agg()之內,代表我們想將符合各種條件的元素,各自集合為一個Polars的List型態(不是Python的list型別)。
  • pl.DataFrame.sort()以括號內的expr做為排序依據並以升冪型式呈現。pl.col("len")是一個expr,代表「"len"」列。

Polars讓我們可以使用pl.DataFrame.group_by().agg()來進行聚合計算,相比於itertools.groupby,更加容易使用。請留意,我們仍然是以為中心來思考,只是需要將代表聚合的expr置入pl.DataFrame.group_by().agg()中。

4. 容器內元素選取方式(*4)

假設我們將nameslucky_numbers列表合併為一個data字典:

data = {"name": names, "lucky_number": lucky_numbers}

我們可以使用data["name"]data["lucky_number"]來取得nameslucky_numbers列表。

如果是使用Polars來操作的話,除了基本的pl.col()選取方式外,還可以使用多種selector來選取,且selector間還可以進行set operation。例如:

df = pl.DataFrame(data)
df.select(cs.by_name("name") | cs.numeric())
shape: (5, 2)
┌───────┬──────────────┐
│ name  ┆ lucky_number │
│ ---   ┆ ---          │
│ str   ┆ i64          │
╞═══════╪══════════════╡
│ May   ┆ 5            │
│ Jeff  ┆ 93           │
│ Cathy ┆ 42           │
│ Jack  ┆ 55           │
│ David ┆ 74           │
└───────┴──────────────┘

這裡我們使用pl.DataFrame.select()這個context來選取列。其中cs.by_name("name")選取到了「"names"」列,而cs.numeric()選取到「"len"」列。最後兩個selector的結果在進行|運算後,選擇到「"names"與「"len"」兩列。

Python原生資料結構的選取方法十分便利,但Polars提供了豐富的selector,提供使用者更多彈性的選擇方法。而所有的一切,仍然是以為中心來思考。

5. 實際範例

最後我想舉一個實際看到的例子,這是PyBites創辦人之一,Bob Belderbos,發表在LinkedIn的一則貼文。其目標是希望能計算多篇文章中,各種tag使用的次數。

Bob只用了三行即完成計算,並使用了Python的Pandas、itertools模組及collections模組,可以看出他對Python的熟悉程度。

import itertools
import collections


df_text = pl.DataFrame(
    {
        "text": [
            "Tags: #Coding #ProblemSolving",
            "Tags: #OpenSource #Collaboration #Efficiency",
            "Tags: #ProblemSolving #Efficiency",
        ]
    }
)

tags = df_text["text"].str.extract_all(r"#\w+").to_list()
tags_flattened = (
    tag.lower() for tag in itertools.chain.from_iterable(tags)
)
most_common_tags = collections.Counter(tags_flattened)
print(most_common_tags)
Counter({'#problemsolving': 2,
         '#efficiency': 2,
         '#coding': 1,
         '#opensource': 1,
         '#collaboration': 1})

但是如果使用Polars,我們可以使用更簡潔的寫法得到答案。例如:

(
    df_text.select(
        pl.col("text")
        .str.extract_all(r"#\w+")
        .list.eval(pl.element().str.to_lowercase())
        .explode()
        .value_counts(sort=True)
        .struct.unnest()
    )
)
shape: (5, 2)
┌─────────────────┬───────┐
│ text            ┆ count │
│ ---             ┆ ---   │
│ str             ┆ u32   │
╞═════════════════╪═══════╡
│ #problemsolving ┆ 2     │
│ #efficiency     ┆ 2     │
│ #coding         ┆ 1     │
│ #opensource     ┆ 1     │
│ #collaboration  ┆ 1     │
└─────────────────┴───────┘

雖然您現在可能看不懂上述程式碼,但我們可以將其與前段程式碼進行比較如下:

  • 正則表達式所萃取的結果不需要先轉換為列表。
  • 使用.list.eval()替代迴圈。
  • 使用Polars提供的資料結構(pl.Expr.value_counts()會返回pl.Struct)來替代collection.Counter

如果我們改變心態,盡量減少使用純Python來操作,而多使用Polars提供的各種功能,就能夠享受更多其所帶來的效能。

6. codepanda

*1. 重覆類似操作

Pandas可以使用pd.DataFrame.assign()來新增或修改列:

lucky_numbers = [5, 93, 42, 55, 74]
df_lucky_numbers = pd.DataFrame({"lucky_number": lucky_numbers})

(
    df_lucky_numbers.assign(
        lucky_number=lambda df_: df_.lucky_number.add(1)
    )
)
   lucky_number
0             6
1            94
2            43
3            56
4            75

*2. 條件選擇

Pandas可以使用pd.DataFrame.query()來篩選行:

lucky_numbers = [5, 93, 42, 55, 74]
df_lucky_numbers = pd.DataFrame({"lucky_numbers": lucky_numbers})

df_lucky_numbers.query("lucky_numbers > 50")
   lucky_number
1            93
3            55
4            74

*3. 分組聚合

Pandas可以使用pd.DataFrame.groupby().agg()來進行分組聚合:

names = ["May", "Jeff", "Cathy", "Jack", "David"]
df_names = pd.DataFrame({"names": names})

(
    df_names.assign(len=lambda df_: df_.names.str.len())
    .groupby("len")
    .agg(list)
    .reset_index()
)
   len            name
0    3           [May]
1    4    [Jeff, Jack]
2    5  [Cathy, David]

*4. 容器內元素選取方式

Pandas可以使用pd.DataFrame.loc來選擇單列或多列:

names = ["May", "Jeff", "Cathy", "Jack", "David"]
lucky_numbers = [5, 93, 42, 55, 74]
data = {"names": names, "lucky_numbers": lucky_numbers}
df = pl.DataFrame(data)

df.loc[:, ["names", "lucky_numbers"]]
    name  lucky_number
0    May             5
1   Jeff            93
2  Cathy            42
3   Jack            55
4  David            74

備註

註1:這邊需留意,使用itertools.groupby時,需先進行排序,否則其預設的邏輯是當遇到不同情況時,即視為前一組別聚合完畢。舉例來說:

groups, uniquekeys = [], []

for k, g in groupby(names, key=len):
    groups.append(list(g))
    uniquekeys.append(k)

print(f"{uniquekeys=}")
print(f"{groups=}")
uniquekeys=[3, 4, 5, 4, 5]
groups=[['May'], ['Jeff'], ['Cathy'], ['Jack'], ['David']]

此例中的names未先進行排序,且由於相鄰的名字剛好為不同長度,所以會產生五組結果。

Code

本日程式碼傳送門


上一篇
[Day02] - 行前準備
下一篇
[Day04] - pl.Series與pl.DataFrame
系列文
Polars熊霸天下8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言